//
//  FLexboxEnums.swift
//  MacDemo
//
//  Created by FH on 2020/8/18.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

#if os(OSX)
import Cocoa
#else
import UIKit
#endif

enum FLValue {
    case undefined
    case numeric(Float)
    case percent(Float)
}

extension FLValue : ExpressibleByIntegerLiteral, ExpressibleByFloatLiteral {
    public init(integerLiteral value: Int) {
        self = value < 0 ? .undefined : .numeric(Float(value))
    }

    public init(floatLiteral value: Float) {
        self = value < 0 ? .undefined : .numeric(value)
    }
    
    public init(_ value: Int) {
        self = value < 0 ? .undefined : .numeric(Float(value))
    }

    public init(_ value: Float) {
        self = value < 0 ? .undefined : .numeric(value)
    }
    
    public init(_ value: Double) {
        self = value < 0 ? .undefined : .numeric(Float(value))
    }

    public init(_ value: CGFloat) {
        self = value < 0 ? .undefined : .numeric(Float(value))
    }
    
    var hasValue: Bool {
        switch self {
        case .undefined:
            return false
        default:
            return true
        }
    }
    
    var isNumeric: Bool {
        if hasValue, case FLValue.numeric(_) = self {
            return true
        }
        return false
    }
    
    var value: Float {
        switch self {
        case .numeric(let val):
            return val
        case .percent(let val):
            return val
        default:
            return 0
        }
    }
}

extension FLValue : Hashable, Equatable {
    func hash(into hasher: inout Hasher) {
        switch self {
        case .numeric(let nVal):
            hasher.combine("value-\(nVal)")
        case .percent(let pVal):
            hasher.combine("percent-\(pVal)")
        default:
            return
        }
    }
    
    static func ==(lhs: FLValue, rhs: FLValue) -> Bool {
        if case FLValue.undefined = lhs {
            return false
        } else if case FLValue.undefined = rhs {
            return false
        }
        return lhs.hashValue == rhs.hashValue
    }
}

enum FLDisplay : Int {
    case flex
    case none
}

enum FLDirection : Int {
    case column
    case columnReverse
    case row
    case rowReverse
}

enum FLAxisDirection : Int {
    case axisRow
    case axisColumn
    
    func invert() -> FLAxisDirection {
        switch self {
        case .axisRow:
            return .axisColumn
        case .axisColumn:
            return .axisRow
        }
    }
}

enum FLPosition : Int {
    case relative
    case absolute
}

enum FLWrap : Int {
    case noWrap
    case wrap
    case wrapReverse
}

enum FLAlign : Int {
    case auto
    case start
    case center
    case end
    case stretch
    case baseline
    case between
    case around
}

enum FLJustify : Int {
    case start
    case center
    case end
    case between
    case around
}

enum FLMeasure : Int {
    case undefined
    case exactly
    case atMost
}

//////////////////////////////////////////

class FLAxis : Copyable {
    required init(instance: FLAxis) {
        self.isRowDirection = instance.isRowDirection
        self.dim = instance.dim
        self.measure = instance.measure
    }
    
    var isRowDirection: Bool
    var dim: FLValue
    var measure: FLMeasure
    var axisDirection: FLAxisDirection {
        return isRowDirection ? .axisRow : .axisColumn
    }

    init(isRowDirection: Bool,
         dim: FLValue,
         measure: FLMeasure) {
        self.isRowDirection = isRowDirection
        self.dim = dim
        self.measure = measure
    }
}

extension FLAxis : CustomDebugStringConvertible {
    var debugDescription: String {
        return "Axis's direction is \(isRowDirection ? "row" : "column"), dim: \(dim.value), measure: \(measure)"
    }
}

extension FLAxis : Hashable, Equatable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(isRowDirection)
        hasher.combine(dim)
        hasher.combine(measure)
    }

    static func == (lhs: FLAxis, rhs: FLAxis) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }
}

class FLDimensions : Copyable, CustomDebugStringConvertible {
    var debugDescription: String {
        return """
        Width: \(widthAxis)
        Height: \(heightAxis)
        """
    }
    
    required init(instance: FLDimensions) {
        self.wrap = instance.wrap
        self.alignItems = instance.alignItems
        self.mainAxis = instance.mainAxis.copy()
        self.crossAxis = instance.crossAxis.copy()
    }
    
    let wrap: FLWrap
    let alignItems: FLAlign
    let mainAxis: FLAxis
    let crossAxis: FLAxis
    
    var isMainAxisRow: Bool {
        return mainAxis.isRowDirection
    }
    
    var isMeasuredExactly: Bool {
        return widthAxis.measure == .exactly && heightAxis.measure == .exactly
    }
    
    var hasValues: Bool {
        return widthAxis.dim.value > 0 && heightAxis.dim.value > 0
    }
    
    var isEmptyValues: Bool {
        return widthAxis.dim.value == 0 && heightAxis.dim.value == 0
    }
    
    var widthAxis: FLAxis {
        return mainAxis.isRowDirection ? mainAxis : crossAxis
    }
    
    var heightAxis: FLAxis {
        return mainAxis.isRowDirection ? crossAxis : mainAxis
    }
    
    init(direction: FLDirection = .column,
         width: FLValue = .undefined,
         widthMeasure: FLMeasure = .undefined,
         height: FLValue = .undefined,
         heightMeasure: FLMeasure = .undefined,
         alignItems: FLAlign = .auto,
         wrap: FLWrap = .noWrap) {
        self.wrap = wrap
        self.alignItems = alignItems
        if direction == .row || direction == .rowReverse {
            mainAxis = FLAxis(isRowDirection: true, dim: width, measure: widthMeasure)
            crossAxis = FLAxis(isRowDirection: false, dim: height, measure: heightMeasure)
        } else {
            mainAxis = FLAxis(isRowDirection: false, dim: height, measure: heightMeasure)
            crossAxis = FLAxis(isRowDirection: true, dim: width, measure: widthMeasure)
        }
    }
    
    func measureMode() -> (Width: FLMeasure, Height: FLMeasure) {
        return (widthAxis.measure, heightAxis.measure)
    }
    
    // TODO: 梳理availableDimensions、availableInnerDimensions、innerSize逻辑。innerSize只能内部使用
    func availableDimensions(by node: FLNode) -> FLDimensions {
        let copy = self.copy()
        copy.widthAxis.dim = FLValue(widthAxis.dim.value + node.marginForAxisRow)
        copy.heightAxis.dim = FLValue(heightAxis.dim.value + node.marginForAxisColumn)
        return copy
    }
    
    func innerSize(by node: FLNode) -> (Width: Float, Height: Float) {
        return (
            widthAxis.dim.value - node.marginForAxisRow - node.paddingForAxisRow,
            heightAxis.dim.value - node.marginForAxisColumn - node.paddingForAxisColumn
        )
    }
    
    func availableInnerDimensions(by node: FLNode,
                                  parentDimensions: FLDimensions,
                                  boundAxisSizeFn: (FLNode,
                                                    (Width: Float, Height: Float),
                                                    (Width: Float, Height: Float)) -> (Width: Float, Height: Float))
        -> FLDimensions {
        let copy = self.copy()
        let innerSize = self.innerSize(by: node)
        let boundSize = boundAxisSizeFn(node, innerSize, parentDimensions.toSize())
        copy.widthAxis.dim = FLValue(boundSize.Width)
        copy.heightAxis.dim = FLValue(boundSize.Height)
        return copy
    }
    
    func prepareForMeasure() {
        widthAxis.measure = .exactly
        heightAxis.measure = .exactly
    }
    
    func axisSameParent(axisDirection: FLAxisDirection) -> FLAxis {
        return axisDirection == .axisRow ? widthAxis : heightAxis
    }
    
    @discardableResult
    func setAlignStretch(forceUpdate: Bool=false,
                         for child: FLNode,
                         childAvailableDimensions: FLDimensions,
                         boundAxisSizeFn: (FLNode,
                                            (Width: Float, Height: Float),
                                            (Width: Float, Height: Float)) -> (Width: Float, Height: Float)) -> Bool {
        let sameDirection = childAvailableDimensions.axisSameParent(axisDirection: self.mainAxis.axisDirection.invert())
        if (forceUpdate || sameDirection.measure != .exactly)
            && self.crossAxis.measure == .exactly
            && (self.alignItems == .stretch || child.alignSelf == .stretch) {
            let availableSize = (
                Width: sameDirection.isRowDirection ?
                    self.widthAxis.dim.value - child.marginForAxisRow :
                    childAvailableDimensions.widthAxis.dim.value,
                Height: !sameDirection.isRowDirection ?
                    self.heightAxis.dim.value - child.marginForAxisColumn :
                    childAvailableDimensions.heightAxis.dim.value
            )
            let boundSize = boundAxisSizeFn(child, availableSize, self.toSize())
            let updateDim = FLValue(sameDirection.isRowDirection ? boundSize.Width : boundSize.Height)
            let needUpdate = forceUpdate ? (updateDim.value > sameDirection.dim.value) : true
            if needUpdate {
                sameDirection.dim = updateDim
                if sameDirection.measure == .atMost {
                    sameDirection.measure = .exactly
                }
                return true
            }
        }
        return false
    }
    
    func toChild(value: FLValue, forWidth: Bool = true) -> FLValue {
        switch value {
        case .undefined, .numeric:
            return value
        case .percent(let val):
            return FLValue(val * (forWidth ? widthAxis.dim.value : heightAxis.dim.value))
        }
    }
    
    func toSize() -> (Width: Float, Height: Float) {
        return (widthAxis.dim.value, heightAxis.dim.value)
    }
}

extension FLDimensions : Hashable, Equatable {
    func hash(into hasher: inout Hasher) {
        hasher.combine(widthAxis)
        hasher.combine(heightAxis)
        hasher.combine(wrap)
        hasher.combine(alignItems)
    }
    
    func equalSize(to rhs: FLDimensions) -> Bool {
        return self.widthAxis.dim.value == rhs.widthAxis.dim.value
            && self.heightAxis.dim.value == rhs.heightAxis.dim.value
    }
    
    static func == (lhs: FLDimensions, rhs: FLDimensions) -> Bool {
        // TODO: remove
        if lhs.widthAxis.dim.value == rhs.widthAxis.dim.value && lhs.heightAxis.dim.value == rhs.heightAxis.dim.value
        && lhs.hashValue != rhs.hashValue {
            print()
        }
        
        return lhs.hashValue == rhs.hashValue
    }
}

struct FLRenderOrigin {
    let isMainAxisRow: Bool
    let onOriginLeft: Bool
    let onOriginTop: Bool
    let measureSize: (Width: Float, Height: Float)
    let padding: (Left: Float, Right: Float, Top: Float, Bottom: Float)
    var baseOrigin: (x: Float, y: Float)
    
    mutating func updateOriginForNextCrossLine(by node: FLNode,
                                      crossContentDim: Float) {
        var nextOrigin: (x: Float, y: Float) = (0, 0)
        if node.isMainAxisRow {
            nextOrigin = (
                x: baseOrigin.x,
                y: baseOrigin.y + (self.onOriginTop ? 1 : -1) * crossContentDim
            )
        } else {
            nextOrigin = (
                x: baseOrigin.x + (self.onOriginLeft ? 1 : -1) * crossContentDim,
                y: baseOrigin.y
            )
        }
        baseOrigin = nextOrigin
    }
    
    func nextItemOriginInCrossLine(currVew: FLView,
                                   currNode: FLNode) -> (x: Float, y: Float) {
        let currOrigin = currVew.frame.origin.toFloatValue()
        let currSize = currVew.frame.size.toFloatValue()
        var x: Float = -1
        var y: Float = -1
        if isMainAxisRow {
            if onOriginLeft {
                x = currOrigin.x + currSize.Width + currNode.marginRight.value
            } else {
                x = currOrigin.x - currNode.marginLeft.value
            }
        } else {
            if onOriginTop {
                y = currOrigin.y + currSize.Height + currNode.marginBottom.value
            } else {
                y = currOrigin.y - currNode.marginTop.value
            }
        }
        return (x, y)
    }
}

extension FLRenderOrigin {
    static var empty: FLRenderOrigin {
        return FLRenderOrigin(isMainAxisRow: false,
                              onOriginLeft: true,
                              onOriginTop: true,
                              measureSize: (0, 0),
                              padding: (0, 0, 0, 0),
                              baseOrigin: (0, 0))
    }
}

//////////////////////////////////////////

extension CGRect {
    init(x: Float, y: Float, width: Float, height: Float) {
        self.init(x: Double(x), y: Double(y), width: Double(width), height: Double(height))
    }
    
    var bottomRightX: Float {
        return size.width > 0 ? Float(origin.x + size.width) : 0
    }
    
    var bottomRightY: Float {
        return size.height > 0 ? Float(origin.y + size.height) : 0
    }
}

extension CGSize {
    init(width: Float, height: Float) {
        self.init(width: Double(width), height: Double(height))
    }
    
    var hasValue: Bool {
        return width > 0 && height > 0
    }
    
    func toFloatValue() -> (Width: Float, Height: Float) {
        return (Float(width), Float(height))
    }
}

extension CGPoint {
    init(x: Float, y: Float) {
        self.init(x: Double(x), y: Double(y))
    }
    
    var hasValue: Bool {
        return x > 0 && y > 0
    }
    
    func toFloatValue() -> (x: Float, y: Float) {
        return (Float(x), Float(y))
    }
}

#if os(OSX)
extension NSSize {
    func toCGSize() -> CGSize {
        return CGSize(width: self.width, height: self.height)
    }
}
#endif
